/*
	下载、解析（XML）、存储iTunes排行榜到Core Data。
 */

#import "iTunesRSSImporter.h"
#import "Song.h"
#import <libxml/tree.h>

// Function prototypes for SAX callbacks. This sample implements a minimal subset of SAX callbacks.
// Depending on your application's needs, you might want to implement more callbacks.
static void startElementSAX(void *context, const xmlChar *localname, const xmlChar *prefix, const xmlChar *URI, int nb_namespaces, const xmlChar **namespaces, int nb_attributes, int nb_defaulted, const xmlChar **attributes);
static void endElementSAX(void *context, const xmlChar *localname, const xmlChar *prefix, const xmlChar *URI);
static void charactersFoundSAX(void *context, const xmlChar *characters, int length);
static void errorEncounteredSAX(void *context, const char *errorMessage, ...);

// Forward reference. The structure is defined in full at the end of the file.
static xmlSAXHandler simpleSAXHandlerStruct;

// Class extension for private properties and methods.
@interface iTunesRSSImporter ()

@property BOOL storingCharacters;
@property (nonatomic, retain) NSMutableData *characterBuffer;
@property BOOL done;
@property BOOL parsingASong;
@property NSUInteger countForCurrentBatch;
@property (nonatomic, retain) Song *currentSong;
@property (nonatomic, retain) NSURLConnection *rssConnection;
@property (nonatomic, retain) NSDateFormatter *dateFormatter;
// The autorelease pool property is assign because autorelease pools cannot be retained.
@property (nonatomic, assign) NSAutoreleasePool *importPool;

@end

@implementation iTunesRSSImporter

@synthesize iTunesURL, delegate, persistentStoreCoordinator;
@synthesize rssConnection, done, parsingASong, storingCharacters, currentSong, countForCurrentBatch, characterBuffer, dateFormatter, importPool;

- (void)dealloc {
    [iTunesURL release];
    [characterBuffer release];
    [currentSong release];
    [rssConnection release];
    [dateFormatter release];
    [persistentStoreCoordinator release];
    [insertionContext release];
    [songEntityDescription release];
    [super dealloc];
}

- (void)main {
    self.importPool = [[NSAutoreleasePool alloc] init];
	
	// 1）
	// self.insertionContext 发出一个通知 NSManagedObjectContextDidSaveNotification 给观察者 delegate。
	// 并由实现了委托 delegate 的类使用方法委托的方法 importerDidSave: 来处理这个通知
	// 意思是，self.insertionContext已经把数据保存了，主线程的importerDidSave:方法继续做一下步吧。
    if (delegate && [delegate respondsToSelector:@selector(importerDidSave:)]) {
        [[NSNotificationCenter defaultCenter] addObserver:delegate selector:@selector(importerDidSave:) name:NSManagedObjectContextDidSaveNotification object:self.insertionContext];
    }
	
	// 表示数据下载未完成
    done = NO;
	
	// 2）
	// 日期格式化
    self.dateFormatter = [[[NSDateFormatter alloc] init] autorelease];
    [dateFormatter setDateStyle:NSDateFormatterLongStyle];
    [dateFormatter setTimeStyle:NSDateFormatterNoStyle];
    // necessary because iTunes RSS feed is not localized, so if the device region has been set to other than US
    // the date formatter must be set to US locale in order to parse the dates
    [dateFormatter setLocale:[[[NSLocale alloc] initWithLocaleIdentifier:@"US"] autorelease]];
	
	// 3)
	// 初始化一个数据缓存，存放下载的数据
    self.characterBuffer = [NSMutableData data];
	
	// 4)
    // This creates a context for "push" parsing in which chunks of data that are not "well balanced" can be passed
    // to the context for streaming parsing. The handler structure defined above will be used for all the parsing. 
    // The second argument, self, will be passed as user data to each of the SAX handlers. The last three arguments
    // are left blank to avoid creating a tree in memory.
    context = xmlCreatePushParserCtxt(&simpleSAXHandlerStruct, self, NULL, 0, NULL);

	// 5）
	// 使用NSURLConnection下载RSS
    NSURLRequest *theRequest = [NSURLRequest requestWithURL:iTunesURL];
    // create the connection with the request and start loading the data
    rssConnection = [[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
	
	// 没有下面这5行，main方法结束后，NSOperationQueue开启的第二线程随之也结束，
	// 但是NSURLConnection的回调需要依赖该线程的NSRunLoop。
	// 为了避免这种情况，则保持NSRunLoop的运行，直到NSURLConnection回调结束（Done = YES）。
    if (rssConnection != nil) {
        do {
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        } while (!done);
    }
	
	// 6）
    // Release resources used only in this thread.
    xmlFreeParserCtxt(context);
    self.characterBuffer = nil;
    self.dateFormatter = nil;
    self.rssConnection = nil;
    self.currentSong = nil;
	
	// 7)
	// 托管对象上下文保存获取到歌曲信息
    NSError *saveError = nil;
    NSAssert1([insertionContext save:&saveError], @"Unhandled error saving managed object context in import thread: %@", [saveError localizedDescription]);
	
	// 8）同1），歌曲下载完毕，解析完毕，保存完毕，让delegate来处理
    if (delegate && [delegate respondsToSelector:@selector(importerDidSave:)]) {
        [[NSNotificationCenter defaultCenter] removeObserver:delegate name:NSManagedObjectContextDidSaveNotification object:self.insertionContext];
    }
    if (self.delegate != nil && [self.delegate respondsToSelector:@selector(importerDidFinishParsingData:)]) {
        [self.delegate importerDidFinishParsingData:self];
    }
	
    [importPool release];
    self.importPool = nil;
}

// 创建一个托管对象上下文，并于AppDelegate传递来的持久化存储协调器相关联
- (NSManagedObjectContext *)insertionContext {
    if (insertionContext == nil) {
        insertionContext = [[NSManagedObjectContext alloc] init];
        [insertionContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
    }
    return insertionContext;
}

// 创建歌曲实体描述对象，实体名称为：Song
- (NSEntityDescription *)songEntityDescription {
    if (songEntityDescription == nil) {
        songEntityDescription = [[NSEntityDescription entityForName:@"Song" inManagedObjectContext:self.insertionContext] retain];
    }
    return songEntityDescription;
}

// 使用上面的实体描述对象创建一个托管对象（Song是托管对象的一个子类），并注册到上下文中，然后返回这个托管对象
- (Song *)currentSong {
    if (currentSong == nil) {
        currentSong = [[Song alloc] initWithEntity:self.songEntityDescription insertIntoManagedObjectContext:self.insertionContext];
        currentSong.rank = [NSNumber numberWithUnsignedInteger:++rankOfCurrentSong];
    }
    return currentSong;
}

#pragma mark NSURLConnection Delegate methods

// 下载RSS时出错处理
- (void)forwardError:(NSError *)error {
    if (self.delegate != nil && [self.delegate respondsToSelector:@selector(importer:didFailWithError:)]) {
        [self.delegate importer:self didFailWithError:error];
    }
}

// Forward errors to the delegate.
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    [self performSelectorOnMainThread:@selector(forwardError:) withObject:error waitUntilDone:NO];
    // Set the condition which ends the run loop.
    done = YES;
}

// Called when a chunk of data has been downloaded.
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    // 处理下载的数据块。
	// 使用startElementSAX、endElementSAX、charactersFoundSAX、errorEncounteredSAX这4个方法
    xmlParseChunk(context, (const char *)[data bytes], [data length], 0);
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
	// 处理下载的数据块。
	// 注意最后1个参数“1”，表示当前处理的数据块是xml文件的最后部分了
	// 使用startElementSAX、endElementSAX、charactersFoundSAX、errorEncounteredSAX这4个方法
    xmlParseChunk(context, NULL, 0, 1);
    context = NULL;
	
    // Set the condition which ends the run loop.
    done = YES; 
}

#pragma mark Parsing support methods

// 每20首歌曲信息保存一次
static const NSUInteger kImportBatchSize = 20;

- (void)finishedCurrentSong {
    parsingASong = NO;
    self.currentSong = nil;
    countForCurrentBatch++;
	
	// 每20首歌曲信息保存一次
    if (countForCurrentBatch == kImportBatchSize) {
        
        NSError *saveError = nil;
        NSAssert1([insertionContext save:&saveError], @"Unhandled error saving managed object context in import thread: %@", [saveError localizedDescription]);
        countForCurrentBatch = 0;
    }
}

/*
 Character data is appended to a buffer until the current element ends.
 */
- (void)appendCharacters:(const char *)charactersFound length:(NSInteger)length {
    [characterBuffer appendBytes:charactersFound length:length];
}

- (NSString *)currentString {
    // Create a string with the character data using UTF-8 encoding. UTF-8 is the default XML data encoding.
    NSString *currentString = [[[NSString alloc] initWithData:characterBuffer encoding:NSUTF8StringEncoding] autorelease];
    [characterBuffer setLength:0];
    return currentString;
}

@end

#pragma mark SAX Parsing Callbacks
// http://bill.dudney.net/roller/objc/entry/libxml2_push_parsing 介绍如何使用libxml2解析XML的

// The following constants are the XML element names and their string lengths for parsing comparison.
// The lengths include the null terminator, to ensure exact matches.
static const char *kName_Item = "item";
static const NSUInteger kLength_Item = 5;
static const char *kName_Title = "title";
static const NSUInteger kLength_Title = 6;
static const char *kName_Category = "category";
static const NSUInteger kLength_Category = 9;
static const char *kName_Itms = "itms";
static const NSUInteger kLength_Itms = 5;
static const char *kName_Artist = "artist";
static const NSUInteger kLength_Artist = 7;
static const char *kName_Album = "album";
static const NSUInteger kLength_Album = 6;
static const char *kName_ReleaseDate = "releasedate";
static const NSUInteger kLength_ReleaseDate = 12;
static const char *kName_CoverArt = "coverArt";
static const NSUInteger kLength_CoverArt = 9;

/*
 This callback is invoked when the importer finds the beginning of a node in the XML. For this application,
 out parsing needs are relatively modest - we need only match the node name. An "item" node is a record of
 data about a song. In that case we create a new Song object. The other nodes of interest are several of the
 child nodes of the Song currently being parsed. For those nodes we want to accumulate the character data
 in a buffer. Some of the child nodes use a namespace prefix. 
 */
static void startElementSAX(void *parsingContext, const xmlChar *localname, const xmlChar *prefix, const xmlChar *URI, 
                            int nb_namespaces, const xmlChar **namespaces, int nb_attributes, int nb_defaulted, const xmlChar **attributes) {
    iTunesRSSImporter *importer = (iTunesRSSImporter *)parsingContext;
    // The second parameter to strncmp is the name of the element, which we known from the XML schema of the feed.
    // The third parameter to strncmp is the number of characters in the element name, plus 1 for the null terminator.
    if (prefix == NULL && !strncmp((const char *)localname, kName_Item, kLength_Item)) {
        importer.parsingASong = YES;
    } else if (importer.parsingASong && ( (prefix == NULL && (!strncmp((const char *)localname, kName_Title, kLength_Title) || !strncmp((const char *)localname, kName_Category, kLength_Category))) || ((prefix != NULL && !strncmp((const char *)prefix, kName_Itms, kLength_Itms)) && (!strncmp((const char *)localname, kName_Artist, kLength_Artist) || !strncmp((const char *)localname, kName_Album, kLength_Album) || !strncmp((const char *)localname, kName_ReleaseDate, kLength_ReleaseDate) || !strncmp((const char *)localname, kName_CoverArt, kLength_CoverArt))) )) {
        importer.storingCharacters = YES;
    }
}

/*
 This callback is invoked when the parse reaches the end of a node. At that point we finish processing that node,
 if it is of interest to us. For "item" nodes, that means we have completed parsing a Song object. We pass the song
 to a method in the superclass which will eventually deliver it to the delegate. For the other nodes we
 care about, this means we have all the character data. The next step is to create an NSString using the buffer
 contents and store that with the current Song object.
 */
static void endElementSAX(void *parsingContext, const xmlChar *localname, const xmlChar *prefix, const xmlChar *URI) {    
    iTunesRSSImporter *importer = (iTunesRSSImporter *)parsingContext;
    if (importer.parsingASong == NO) return;
    if (prefix == NULL) {
        if (!strncmp((const char *)localname, kName_Item, kLength_Item)) {
            [importer finishedCurrentSong];
        } else if (!strncmp((const char *)localname, kName_Title, kLength_Title)) {
            importer.currentSong.title = importer.currentString;
        } else if (!strncmp((const char *)localname, kName_Category, kLength_Category)) {
            importer.currentSong.category = importer.currentString;
        }
    } else if (!strncmp((const char *)prefix, kName_Itms, kLength_Itms)) {
        if (!strncmp((const char *)localname, kName_Artist, kLength_Artist)) {
            importer.currentSong.artist = importer.currentString;
        } else if (!strncmp((const char *)localname, kName_Album, kLength_Album)) {
            importer.currentSong.album = importer.currentString;
        } else if (!strncmp((const char *)localname, kName_ReleaseDate, kLength_ReleaseDate)) {
            NSString *dateString = importer.currentString;
            importer.currentSong.releaseDate = [importer.dateFormatter dateFromString:dateString];
        } else if (!strncmp((const char *)localname, kName_CoverArt, kLength_CoverArt)) {
			NSString *cover = importer.currentString;
			NSRange range = [cover rangeOfString:@"60x60"];
			
			if (range.location != NSNotFound)
			{
				importer.currentSong.coverArt = cover;
			}
        }
    }
	
    importer.storingCharacters = NO;
}

/*
 This callback is invoked when the parser encounters character data inside a node. The importer class determines how to use the character data.
 */
static void charactersFoundSAX(void *parsingContext, const xmlChar *characterArray, int numberOfCharacters) {
    iTunesRSSImporter *importer = (iTunesRSSImporter *)parsingContext;
    // A state variable, "storingCharacters", is set when nodes of interest begin and end. 
    // This determines whether character data is handled or ignored. 
    if (importer.storingCharacters == NO) return;
	
    [importer appendCharacters:(const char *)characterArray length:numberOfCharacters];
}

/*
 A production application should include robust error handling as part of its parsing implementation.
 The specifics of how errors are handled depends on the application.
 */
static void errorEncounteredSAX(void *parsingContext, const char *errorMessage, ...) {
    // Handle errors as appropriate for your application.
    NSCAssert(NO, @"Unhandled error encountered during SAX parse.");
}

// The handler struct has positions for a large number of callback functions. 
// If NULL is supplied at a given position, that callback functionality won't be used. 
// Refer to libxml documentation at http://xmlsoft.org/html/libxml-tree.html
// for more information about the SAX callbacks.
static xmlSAXHandler simpleSAXHandlerStruct = {
	NULL,                       /* internalSubset */
	NULL,                       /* isStandalone   */
	NULL,                       /* hasInternalSubset */
	NULL,                       /* hasExternalSubset */
	NULL,                       /* resolveEntity */
	NULL,                       /* getEntity */
	NULL,                       /* entityDecl */
	NULL,                       /* notationDecl */
	NULL,                       /* attributeDecl */
	NULL,                       /* elementDecl */
	NULL,                       /* unparsedEntityDecl */
	NULL,                       /* setDocumentLocator */
	NULL,                       /* startDocument */
	NULL,                       /* endDocument */
	NULL,                       /* startElement*/
	NULL,                       /* endElement */
	NULL,                       /* reference */
	charactersFoundSAX,         /* characters */
	NULL,                       /* ignorableWhitespace */
	NULL,                       /* processingInstruction */
	NULL,                       /* comment */
	NULL,                       /* warning */
	errorEncounteredSAX,        /* error */
	NULL,                       /* fatalError //: unused error() get all the errors */
	NULL,                       /* getParameterEntity */
	NULL,                       /* cdataBlock */
	NULL,                       /* externalSubset */
	XML_SAX2_MAGIC,             //
	NULL,
	startElementSAX,            /* startElementNs */
	endElementSAX,              /* endElementNs */
	NULL,                       /* serror */
};
